// This files defines the grammar that's used by [Peggy](https://peggyjs.org/) to generate the searchParser.js file.
// The searchParser is setup to parse our custom search syntax and output an AST with the filters.
//
// Here's a general grammar structure:
//
// query: entry point for the parser and rule to process the values returned by the filterList rule. Takes filters as an argument and returns the final AST output.
// filterList: rule to process the array of filters returned by the filter rule. It takes head and tail as arguments, filters it for null values and builds the AST.
// filter: abstract rule to simplify the filterList rule. It takes all filter types.
// defaultFilter: rule to process the default values returned by the defaultKey rule. It updates the default values object.
// freeTextFilter: rule to process the free text search values returned by the identifier rule. It builds filter Object.
// standardFilter: rule to process the values returned by the key rule. It builds filter Object.
// key: rule to match pre-defined search syntax fields, e.g. amount, merchant, etc
// defaultKey: rule to match pre-defined search syntax fields that are used to update default values, e.g. type, status, etc
// identifier: composite rule to match patterns defined by the quotedString and alphanumeric rules

// filter, logicalAnd, operator, alphanumeric, quotedStrig are defined in baseRules.peggy grammar

// global initializer (code executed only once)
{{// CAUTION: DO NOT DIRECTLY ALTER OR MODIFY `searchParser.js` OR `autocompleteParser.js`
// These files are auto-generated by Peggy from grammar files (*.peggy). 
// To make changes, edit the corresponding *.peggy files only.
// Use the `generate-search-parser` and `generate-autocomplete-parser` scripts to regenerate parsers after modifications.

  function buildFilter(operator, left, right) {
    return { operator, left, right };
  }
}}

// per-parser initializer (code executed before every parse)
{
  const defaultValues = {
    type: "expense",
    status: "",
    sortBy: "date",
    sortOrder: "desc",
  };
  let rawFilterList = [];

  // List fields where you cannot prefix it with "-" to negate it
  const nonNegatableKeys = new Set([
    "type", "keyword", "groupCurrency", "groupBy"
  ]);

  function applyDefaults(filters) {
    return {
      ...defaultValues,
      filters,
      rawFilterList,
    };
  }

  function updateDefaultValues(field, value) {
    if (field === "status" && value === "all") {
      defaultValues[field] = "";
      return;
    }
    defaultValues[field] = value;
  }

  function createRawFilter(filter) {
    if (!filter || !filter.right) {
      return null;
    }

    if (typeof filter.left !== "string") {
      return null;
    }

    const key = filter.left;
    const value = Array.isArray(filter.right) ? [...filter.right] : filter.right;

    return {
      key,
      operator: filter.operator,
      value,
      isDefault: !!filter.isDefault,
    };
  }
}

query = _ filters:filterList? _ { return applyDefaults(filters); }

filterList
  = head:filter tail:(logicalAnd filter)* {
      const allFilters = [head, ...tail.map(([_, filter]) => filter)]
        .filter(Boolean)
        .filter((filter) => filter.right);
      rawFilterList = allFilters.map(createRawFilter).filter(Boolean);

      const filtersWithoutDefaults = allFilters.filter((filter) => !filter.isDefault);

      if (!filtersWithoutDefaults.length) {
        return null;
      }

      const keywords = filtersWithoutDefaults.filter(
        (filter) =>
          filter.left === "keyword" || filter.right?.left === "keyword"
      );
      const nonKeywords = filtersWithoutDefaults.filter(
        (filter) =>
          filter.left !== "keyword" && filter.right?.left !== "keyword"
      );

      const keywordFilter = buildFilter(
        "eq",
        "keyword",
        keywords
          .map((filter) => filter.right.replaceAll(/^(['"])(.*)\1$/g, "$2"))
          .flat()
      );
      if (keywordFilter.right.length > 0) {
        nonKeywords.push(keywordFilter);
      }
      return nonKeywords.reduce((result, filter) =>
        buildFilter("and", result, filter)
      );
    }

filter = @(standardFilter / defaultFilter / freeTextFilter)

defaultFilter
  = _ key:defaultKey _ op:operator _ value:identifier {
      updateDefaultValues(key, value);
      return {...buildFilter(op, key, value), isDefault: true};
    }

freeTextFilter
  = _ value:(quotedString / [^ \t\r\n\xA0]+) _ {
      //handle no-breaking space
      let word;
      if (Array.isArray(value)) {
        word = value.join("");
      } else {
        word = value;
      }
      return buildFilter("eq", "keyword", word);
    }

standardFilter
  = _ neg:"-"? field:filterKey _ op:filterOperator _ values:identifier {
      expectingNestedQuote = false; nameOperator = false;

      const key = field;
      let operator = op;

      // Apply negation only when "-" is present and the key can be negated
      if (neg && !nonNegatableKeys.has(key)) {
        if (operator === "eq") {
          operator = "neq";
        }
      }

      return buildFilter(operator, key, values);
    }

reportFieldDynamic
  = ("report-field"i / "reportfield"i) "-" rest:$((!([ \t\r\n\xA0,:=<>!]) .)*) {
      const suffix = rest.replaceAll(/^-+/g, "");
      return "reportField-" + (suffix ? suffix : "");
    }
    
key "key"
  = @(
      date
      / amount
      / total
      / merchant
      / description
      / reportID
      / keyword
      / in
      / currency
      / groupCurrency
      / tag
      / category
      / to
      / taxRate
      / cardID
      / from
      / attendee
      / payer
      / exporter
      / expenseType
      / withdrawalType
      / withdrawalID
      / submitted
      / approved
      / paid
      / exported
      / posted
      / withdrawn
      / feed
      / title
      / assignee
      / createdBy
      / reimbursable
      / billable
      / action
      / has
      / is
      / purchaseCurrency
      / purchaseAmount
      / reportFieldDynamic
    )

filterKey
  = k:key {
      nameOperator = (k === "from" || k === "to" || k === "payer" || k === "exporter" || k === "attendee" || k === "createdBy" || k === "assignee");
      return k;
    }
    
defaultKey "default key" = @(type / status / sortBy / sortOrder / policyID / groupBy)

identifier
  = (","+)? parts:(values / quotedString / alphanumeric)|1.., ","+| empty:(","+)? {
      const value = parts.flat().filter(Boolean).map((word) => {
        if (word.startsWith('"') && word.endsWith('"') && word.length >= 2) {
          return word.slice(1, -1);
        }
        return word;
      });
      if (value.length > 1) {
        return value.filter((word) => word.length > 0);
      }
      return value[0];
    }
